전처리기 문법 총정리 — 지시자·# 연산자·가변 인자 매크로

NOTE

전처리기는 C 본문과 별개의 작은 언어다. 컴파일 전에 텍스트를 치환하는 단계라서 타입도 변수도 모르고 토큰만 다룬다. CHK_PTR 매크로의 #_p가 무엇인지 궁금해서 시작한 정리이며, 실습에서 이 문법을 쓰는 곳은 마지막 절에 링크로 모았다.

0. 예시로 시작 — #_p는 무엇인가

#define CHK_PTR(_p)                                                     \
    do {                                                                \
        if ((_p) == NULL) {                                             \
            LOG_ERR("%s is NULL (%s/%d)", #_p, __func__, __LINE__);     \
            return RET_INVALID_ARG;                                     \
        }                                                               \
    } while (0)

CHK_PTR(buf);로 호출하면 #_p가 "buf"로 바뀐다. 로그에 buf is NULL (func_name/42)처럼 어느 변수가 NULL이었는지 이름이 남는다. 이것이 아래 2절의 문자열화 연산자다.

1. 지시자 (#으로 시작하는 줄)

지시자용도
#include <x.h> / "x.h"파일 내용을 그 자리에 붙인다. <>는 시스템 경로, ""는 현재 폴더 먼저
#define NAME value객체형 매크로 (상수 치환)
#define F(a,b) expr함수형 매크로
#undef NAME매크로 정의 해제
#if / #elif / #else / #endif조건부 컴파일 (정수 상수식)
#ifdef / #ifndef정의 여부로 분기
#error "msg"컴파일을 일부러 중단
#pragma ...컴파일러 전용 지시 (#pragma once 등)

defined(NAME)은 #if 안에서 정의 여부를 묻는 연산자다. 예: #if defined(_WIN32) && !defined(DEBUG). 조건부 컴파일의 자세한 패턴은 전처리기 조건부 컴파일에 있다.

2. 매크로 연산자 2개

2-1. #x — 문자열화 (stringification)

매크로 인자를 토큰 그대로 문자열 리터럴로 바꾼다.

#define SHOW(x)  printf("%s = %d\n", #x, (x))
SHOW(a + 1);     // printf("%s = %d\n", "a + 1", (a + 1));
  • 매크로 본문 안에서, 매크로 인자 앞에만 쓸 수 있다.
  • 표현식도 그대로 문자열이 된다. CHK_PTR(ctx->name)이면 "ctx->name".
  • 인자가 다른 매크로여도 확장되기 전 이름이 찍힌다. 확장된 결과가 필요하면 한 단계 더 감싼다.
#define STR(x)   #x
#define XSTR(x)  STR(x)
#define VER 3
STR(VER)    // "VER"
XSTR(VER)   // "3"

2-2. a ## b — 토큰 이어붙이기

두 토큰을 하나의 토큰으로 합친다.

#define MAKE_VAR(n)  var_##n
int MAKE_VAR(1);   // int var_1;

3. 가변 인자 매크로

#define LOG(fmt, ...)  printf("[LOG] " fmt "\n", __VA_ARGS__)
  • ...이 가변 인자 자리이고 __VA_ARGS__가 그 내용이다.
  • LOG("hi")처럼 가변 인자가 0개면 , __VA_ARGS__의 쉼표가 남아 컴파일이 깨진다.
  • 해결: GNU 확장 ##__VA_ARGS__(앞 쉼표를 지움) 또는 C23의 __VA_OPT__(,).
  • 주의: -pedantic -Werror와 ##__VA_ARGS__는 충돌한다 (clang -Wgnu-zero-variadic-macro-arguments).
  • 함수로 가변 인자를 받는 방법은 가변 인자 함수 참고.

4. 미리 정의된 매크로

매크로값
__FILE__파일 이름 (문자열)
__LINE__줄 번호 (정수)
__func__함수 이름 (C99). 전처리기가 아니라 컴파일러 기능이라 #ifdef로 검사할 수 없다
__DATE__ / __TIME__컴파일 시각
__STDC_VERSION__C 표준 버전 (201112L = C11)
_WIN32, __linux__ 등플랫폼 판별 (컴파일러가 정의)

5. 관용구와 함정

  1. 인클루드 가드 — #ifndef X_H / #define X_H / #endif. 같은 헤더가 두 번 들어가는 것을 막는다.

  2. do { ... } while (0) — 여러 문장 매크로를 if (x) MACRO(); else ...에서도 안전하게 쓰게 한다. 매크로를 do-while(0)으로 감싸는 이유 참고.

  3. 인자와 전체를 괄호로 감싸기

    #define SQ(x) x*x        // SQ(a+1) → a+1*a+1  (오답)
    #define SQ(x) ((x)*(x))  // 정답
  4. 부작용이 있는 인자는 여러 번 평가된다 — SQ(i++)는 i++가 두 번 실행된다. 매크로는 값이 아니라 텍스트를 복사하기 때문이다.

  5. 매크로 안에 return을 숨기면 반환형이 맞는 함수에서만 쓸 수 있다. CHK_PTR는 ret_t/int 반환 함수 전용이다.

  6. 세미콜론은 호출하는 쪽에서 붙인다 — 매크로 안에 ;를 넣으면 일반 문장처럼 읽히지 않는다.

  7. 타입 검사가 없다 — 가능하면 static inline 함수와 enum을 우선한다.

  8. #ifdef 이름 오타는 조용히 “정의 안 됨” — 경고 없이 해당 코드가 빠진다.

6. 직접 확인하기

전처리 결과만 출력해 매크로가 어떻게 전개되는지 눈으로 볼 수 있다.

gcc -E file.c      # 또는 clang -E file.c

CHK_PTR, LOG_PRINT 같은 매크로를 만들면 한 번 돌려 보는 것이 가장 빠른 이해법이다.

7. 실습에서 쓰이는 곳

문법실습 노트위치
#_p 문자열화 + do-while(0) + 인클루드 가드D0. 워밍업CHK_PTR, 완료 기준에서 s is NULL 출력 확인
##__VA_ARGS__ 가변 인자 매크로D0. 워밍업LOG_PRINT, -pedantic 충돌 경고
__func__ / __LINE__ 를 쓰는 시험 매크로D1. 자원 없는 함수T_EQ_INT, T_EQ_STR
_Static_assert, offsetof, FSZ 매크로, __VA_ARGS__D2. 표 3장 → 구조체구조체 레이아웃 검증, V_FAIL
#ifdef UNIT_TEST 로 함수 바꿔치기D3. 자원 잡는 함수CALLOC 매크로
#ifdef + 빈 do {} while (0) 로 디버그 코드 제거SD52. 남은 디버그 코드DEBUG_ENABLE, -D 옵션
짝 매크로 (LOCK_BEGIN/LOCK_END)SD48. 매크로의 잘못된 사용인클루드 가드 + 함수형 매크로
NDEBUG 와 assertSD43. 널 포인터 역참조릴리스 빌드에서 assert가 사라지는 문제
#define _DEFAULT_SOURCE (기능 테스트 매크로)SD11. 스택 버퍼 오버플로우 외 다수시험 코드 맨 위, 헤더 include 전에 정의

관련 문서